In [1]:
from IPython.display import Image

Tweets de Elon Musk y su impacto en el valor de las acciónes de Tesla, Inc.

CC5206-1 Introducción a la Minería de Datos

Profesores:

  • Felipe Bravo
  • Barbara Poblete

Integrantes:

  • Gonzalo Maureira
  • Tomás Saldivia
  • Pavan Samtani

Introducción

En el mundo actual las redes sociales se han posicionado como un elemento masivo de intercambio de información. En él, se han hecho partícipes todos los elementos de la sociedad, desde personas comunes hasta celebridades, empresas, políticos, etc.

Históricamente Twitter ha sido una de las redes sociales por excelencia, actualmente no ha dejado de serlo, es más, al 25 de enero cuenta con mas de 320 millones de usuarios activos en el último mes.

A fines del 2018, la Comisión de Seguridad y Energía (SEC) llevó a Elon Musk a juicio. La razón, sus tweets que causaban especulaciones en la Bolsa de Acciones, desde mensajes aludiendo la posible recompra de las acciones de Tesla, sin realmente hacerlo, a especulaciones sobre la demanda. Se estima que Elon Musk ha generado significativas inestabilidades en la bolsa.

In [2]:
Image(filename='musk_1.png')
Out[2]:

Por lo anterior, la SEC argumenta que Elon Musk no debería tener la libertad de hacer tweets sin regulación por parte del equipo legal de la empresa, pero dado que decisiones de ese estilo implican intervenir la libertad de expresión de un hombre, se hace imperativo estudiar si la SEC tiene una base significativa para sus argumentos.

Así, se ha decidido realizar minería de datos, para conocer la influencia que tienen los tweets de una figura tan reconocida como Elon Musk sobre el precio de las acciones de su compañía.

En este proyecto se espera poder responder las siguientes preguntas:

  • ¿Qué relación existe entre los dichos de Musk acerca de Tesla y el precio de acciones en períodos cercanos?
  • ¿Es posible predecir el precio (o tendencia del precio) de las acciones de Tesla en un momento futuro, dado los tweets de Elon Musk e indicadores económicos del período?

A continuación se muestra el código que se hizo para realizar el procesamiento de los datos, junto con acotaciones y análisis que se desprenden de los resultados.

In [3]:
%matplotlib inline

import numpy as np
import pandas as pd
import datetime
import seaborn as sns
import matplotlib.pyplot as plt
import gensim
import spacy
import pyLDAvis.gensim
import os, re, operator, warnings
import spacy

from matplotlib import dates
from gensim.models import CoherenceModel, LdaModel, LsiModel, HdpModel
from gensim.models.wrappers import LdaMallet
from gensim.corpora import Dictionary
from wordcloud import WordCloud, STOPWORDS, ImageColorGenerator
In [4]:
warnings.filterwarnings('ignore')  
nlp = spacy.load('en_core_web_sm')
pd.set_option('display.max_colwidth', -1)

sns.set()
sns.set_context('notebook',font_scale=2.2)
sns.set_style('ticks')
plt.rcParams['figure.figsize'] = (15,8)

Exploración de los Datos

Se obtuvieron 3 datasets con los tweets de Elon Musk en diferentes períodos de tiempo, por lo que se partirá el con el proceso requerido para juntarlos. Cabe destacar que estos datasets se obtuvieron de la página web Kaggle.

In [5]:
ten = pd.read_csv("elonmusk_tweets.csv", delimiter=',', header=0)
print('Muestra del primer dataset:')
display(ten.head())

twelve = pd.read_csv("data_elonmusk.csv", delimiter=',', header=0, encoding="unicode_escape")
print('Muestra del segundo dataset:')
display(twelve.head())

seventeen = pd.read_json("user-tweets.jsonl", lines=True)
print('Muestra del tercer dataset:')
display(seventeen.head())
Muestra del primer dataset:
id created_at text
0 849636868052275200 2017-04-05 14:56:29 b'And so the robots spared humanity ... https://t.co/v7JUJQWfCv'
1 848988730585096192 2017-04-03 20:01:01 b"@ForIn2020 @waltmossberg @mims @defcon_5 Exactly. Tesla is absurdly overvalued if based on the past, but that's irr\xe2\x80\xa6 https://t.co/qQcTqkzgMl"
2 848943072423497728 2017-04-03 16:59:35 b'@waltmossberg @mims @defcon_5 Et tu, Walt?'
3 848935705057280001 2017-04-03 16:30:19 b'Stormy weather in Shortville ...'
4 848416049573658624 2017-04-02 06:05:23 b"@DaveLeeBBC @verge Coal is dying due to nat gas fracking. It's basically dead."
Muestra del segundo dataset:
row ID Tweet Time Retweet from User
0 Row0 @MeltingIce Assuming max acceleration of 2 to 3 g's, but in a comfortable direction. Will feel like a mild to moder? https://t.co/fpjmEgrHfC 2017-09-29 17:39:19 NaN elonmusk
1 Row1 RT @SpaceX: BFR is capable of transporting satellites to orbit, crew and cargo to the @Space_Station and completing missions to the Moon an? 2017-09-29 10:44:54 SpaceX elonmusk
2 Row2 @bigajm Yup :) 2017-09-29 10:39:57 NaN elonmusk
3 Row3 Part 2 https://t.co/8Fvu57muhM 2017-09-29 09:56:12 NaN elonmusk
4 Row4 Fly to most places on Earth in under 30 mins and anywhere in under 60. Cost per seat should be? https://t.co/dGYDdGttYd 2017-09-29 09:19:21 NaN elonmusk
Muestra del tercer dataset:
CreatedAt LinkToTweet Text TweetEmbedCode UserName
0 December 02, 2017 at 07:33PM http://twitter.com/elonmusk/status/937041986304983040 @highqualitysh1t I love the thought of a car drifting apparently endlessly through space and perhaps being discovered by an alien race millions of years in the future <blockquote class="twitter-tweet"><p lang="en" dir="ltr">I love the thought of a car drifting apparently endlessly through space and perhaps being discovered by an alien race millions of years in the future</p>&mdash; Elon Musk (@elonmusk) <a href="https://twitter.com/elonmusk/status/937041986304983040?ref_src=twsrc%5Etfw">December 2, 2017</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> elonmusk
1 December 02, 2017 at 10:46PM http://twitter.com/elonmusk/status/937090715225427968 @novaspivack Asimov's Foundation books should def be part of the mission. They're amazing. <blockquote class="twitter-tweet"><p lang="en" dir="ltr">Asimov's Foundation books should def be part of the mission. They're amazing.</p>&mdash; Elon Musk (@elonmusk) <a href="https://twitter.com/elonmusk/status/937090715225427968?ref_src=twsrc%5Etfw">December 2, 2017</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> elonmusk
2 December 03, 2017 at 12:01AM http://twitter.com/elonmusk/status/937109615698894848 @novaspivack That's certainly the right way to go to store massive amounts of data for a long time <blockquote class="twitter-tweet"><p lang="en" dir="ltr">That's certainly the right way to go to store massive amounts of data for a long time</p>&mdash; Elon Musk (@elonmusk) <a href="https://twitter.com/elonmusk/status/937109615698894848?ref_src=twsrc%5Etfw">December 3, 2017</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> elonmusk
3 December 03, 2017 at 07:05PM http://twitter.com/elonmusk/status/937397330998845440 To preserve the transcendent majesty &amp; specialness of The Boring Company cap, we are capping cap orders at 50,000 caps. Almost there ... https://t.co/YqjEQAfy3u <blockquote class="twitter-tweet"><p lang="en" dir="ltr">To preserve the transcendent majesty &amp; specialness of The Boring Company cap, we are capping cap orders at 50,000 caps. Almost there ... <a href="https://t.co/YqjEQAfy3u">https://t.co/YqjEQAfy3u</a></p>&mdash; Elon Musk (@elonmusk) <a href="https://twitter.com/elonmusk/status/937397330998845440?ref_src=twsrc%5Etfw">December 3, 2017</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> elonmusk
4 December 03, 2017 at 07:07PM http://twitter.com/elonmusk/status/937397813637296128 @harrisonlingren @JW8888888 Busted <blockquote class="twitter-tweet"><p lang="en" dir="ltr">Busted</p>&mdash; Elon Musk (@elonmusk) <a href="https://twitter.com/elonmusk/status/937397813637296128?ref_src=twsrc%5Etfw">December 3, 2017</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> elonmusk

Se observa que en cada dataset se tienen atributos diferentes, por lo tanto se deben guardar los atributos que están presentes en los 3 datasets, esos son: fecha y tweet.

In [6]:
ten = pd.DataFrame({"date": ten["created_at"], "tweet": ten["text"]})
twelve = pd.DataFrame({"date": twelve["Time"], "tweet": twelve["Tweet"]})
seventeen = pd.DataFrame({"date": seventeen["CreatedAt"], "tweet": seventeen["Text"]})

Como se desean juntar todos los datasets, se estandariza la notación para la fecha, eso implica eliminar los segundos del dataset ten y twelve y reformatear seventeen en formato AAAA-MM-DD HH:MM, dado que este formato entrega resultados consistentes de comparación sobre sus strings.

In [7]:
ten["date"] = ten["date"].str[:-3]
twelve["date"] = twelve["date"].str[:-3]
In [8]:
def formatDate(date):
    months = ["January", "February", "March", "April", "May", "June",
             "July", "August", "September", "October", "November", "December"]
    
    date_list = date.split(" ")
    month = "%02d" % (months.index(date_list[0]) + 1)
    day = date_list[1][:-1] #delete ","
    year = date_list[2]
    
    if date_list[4][-2:] == "AM":
        time = date_list[4][:-2]
    else:
        aux = int(date_list[4][:2]) % 12 + 12
        time = ("%02d" % aux) + date_list[4][2:-2]
    
    return year + "-" + month + "-" + day + " " + time
In [9]:
seventeen["date"] = seventeen["date"].apply(lambda x: formatDate(x))

Otra modificación necesaria será eliminar un "b" que se encuentra al principio de todos los tweets del dataset ten

In [10]:
ten["tweet"] = ten["tweet"].str[1:]
ten["tweet"] = ten["tweet"].str[1:-1]

Ahora se deben buscar que rangos de tiempo cubre cada dataset para así juntarlos

In [11]:
print("Dataset ten va desde {} a {}".format(min(ten["date"]), max(ten["date"])))
print("Dataset twelve va desde {} a {}".format(min(twelve["date"]), max(twelve["date"])))
print("Dataset seventeen va desde {} a {}".format(min(seventeen["date"]), max(seventeen["date"])))
Dataset ten va desde 2010-06-04 18:31 a 2017-04-05 14:56
Dataset twelve va desde 2012-11-16 17:59 a 2017-09-29 17:39
Dataset seventeen va desde 2017-12-02 19:33 a 2019-04-08 12:25

Con lo anterior, el único cambio a hacerse es considerar sólo los tweets de twelve que se hayan hecho posterior a 2017-04-05 14:56.

In [12]:
twelve = twelve[twelve["date"] > "2017-04-05 14:56"]

Ahora se puede hacer un sort por fecha de cada dataset (ascendiente) y concatenarlos.

In [13]:
ten = ten.sort_values("date", ascending=True)
twelve = twelve.sort_values("date", ascending=True)
seventeen = seventeen.sort_values("date", ascending=True)
em_tweets = pd.concat([ten, twelve, seventeen])
In [14]:
em_tweets.describe()
Out[14]:
date tweet
count 7176 7176
unique 6993 7152
top 2018-02-22 16:11 RT @SpaceX: Webcast of Falcon 9 launch is now live - https://t.co/gtC39uBC7z
freq 4 3
In [15]:
em_tweets.head()
Out[15]:
date tweet
2818 2010-06-04 18:31 Please ignore prior tweets, as that was someone pretending to be me :) This is actually me.
2817 2011-12-01 09:55 Went to Iceland on Sat to ride bumper cars on ice! No, not the country, Vlad's rink in Van Nuys. Awesome family fun :) http://t.co/rBQXJ9IT
2816 2011-12-01 10:29 I made the volume on the Model S http://t.co/wMCnT53M go to 11. Now I just need to work in a miniature Stonehenge...
2815 2011-12-03 08:20 Great Voltaire quote, arguably better than Twain. Hearing news of his own death, Voltaire replied the reports were true, only premature.
2814 2011-12-03 08:22 That was a total non sequitur btw

Se revisa si hay entradas vacías.

In [16]:
empty_mask = em_tweets[(em_tweets["tweet"] == "") | (em_tweets["date"] == "")]
empty_mask
Out[16]:
date tweet
621 2018-05-18 08:05
3525 2019-03-30 06:02

Se eliminan las entradas vacías

In [17]:
em_tweets.drop(list(empty_mask.index.values), inplace=True)

También se ve que hay tweets repetidos (en el describe del dataframe), así se debe ver si son tweets que se captaron dos veces (tiempos cercanos) o si efectivamente se repitieron por Elon Musk (en cuyo caso no se eliminan). Para esto primero se eliminarán los tweets, que tienen tanto el mismo mensaje como fecha y hora de publicación.

In [18]:
em_tweets.drop(list(em_tweets[em_tweets.duplicated()].index.values), inplace=True)

Ahora se verá manualmente que tweets repetidos hay, para ver si efectivamente se hicieron más de una vez o se captaron más de una vez.

In [19]:
repeated = em_tweets[em_tweets["tweet"].duplicated(keep=False)]
repeated
Out[19]:
date tweet
0 2017-04-05 14:56 And so the robots spared humanity ... https://t.co/v7JUJQWfCv
706 2017-04-05 17:56 And so the robots spared humanity ... https://t.co/v7JUJQWfCv
609 2017-05-16 02:14 RT @SpaceX: Webcast of Falcon 9 launch is now live ? https://t.co/gtC39uBC7z
375 2017-06-23 22:05 RT @SpaceX: Webcast of Falcon 9 launch is now live ? https://t.co/gtC39uBC7z
5 2017-12-03 19:07 @IvanEscobosa Yes
31 2017-12-14 19:26 @IvanEscobosa Yes
38 2017-12-15 15:34 RT @SpaceX: Webcast of Falcon 9 launch is now live - https://t.co/gtC39uBC7z
244 2018-02-22 14:16 RT @SpaceX: Webcast of Falcon 9 launch is now live - https://t.co/gtC39uBC7z
479 2018-04-18 23:48 RT @SpaceX: Webcast of Falcon 9 launch is now live - https://t.co/gtC39uBC7z
2657 2018-12-20 01:36 @wolfejosh
2659 2018-12-20 02:22 @wolfejosh
2871 2019-01-11 03:47 @Erdayastronaut Yes
3001 2019-02-01 02:16 @TriTexan @SpaceX Yes
3031 2019-02-03 07:18 @TriTexan @SpaceX Yes
3238 2019-03-01 07:12 @Erdayastronaut Yes

Viendo las fechas, y revisando twitter manualmente, el único tweet que se consideró más de una vez es el de índice 0, por lo que se eliminará

In [20]:
em_tweets.drop([0], inplace=True)

Hábitos en Twitter de Elon Musk

In [21]:
years = pd.DataFrame({"year": em_tweets[em_tweets["date"] >= "2011-01-01 00:00"]["date"].apply(lambda x: x.split(" ")[0].split("-")[0]).astype("int")})

Dado que hay sólo 1 tweet del 2010, este se ignorará para el histograma

In [22]:
a = pd.DataFrame({"freq": years["year"].value_counts()})
a["year"] = a.index
a.sort_values(["year"], inplace=True)
plt.figure()
plt.gca().invert_yaxis()
plt.barh(a["year"], a["freq"])
plt.grid()
plt.xlabel("Número de Tweets")
plt.ylabel("Año")
plt.yticks(list(a["year"]))
plt.title("Número de Tweets de Elon Musk por año");

Como se ve, la actividad de Elon Musk en Twitter ha aumentado considerablemente con el pasar de los años, aún así no es de fiarse completamente este gráfico, dado que es posible que existan tweets que no se registraron en los datasets. Ahora se estudiarán las horas con más actividad en Twitter por parte de Elon Musk.

In [23]:
timings = pd.DataFrame({"time": em_tweets["date"].apply(lambda x: x.split(" ")[1].split(":")[0] + x.split(" ")[1].split(":")[1])
                        .astype("int")})
hours = np.arange(0, 2401, 100).astype(str)
for ind in range(len(hours)):
    hour = hours[ind]
    if len(hour) == 3:
        hours[ind] = "0"+hour[0]+":"+hour[1:]
    elif len(hour) == 4:
        hours[ind] = hour[0:2]+":"+hour[2:]
    else:
        hours[ind] = "0"+hour+":"+"00" 
In [24]:
timings.hist(bins=range(0, 2401, 100), alpha=0.8, histtype='bar', ec='black', xrot=45)
plt.xlabel("Hora de publicación (Hora Pacífico)")
plt.ylabel("Frecuencia")
plt.xticks(range(0, 2401, 100),hours)
plt.title("Hora de publicación de los tweets de Elon Musk");

Se puede observar que Elon Musk suele twittear todo el día, con peaks durante la tarde y la madrugada, aunque vemos un período en el mediodía en el que también se registró una cantidad significativa de tweets. Una posible hipótesis que explique las horas en que se hacen las publicaciones, son los contantes viajes que realiza Musk, haciendo que se encuentre en distintos husos horarios.

Minería de Texto para explorar los tweets de Musk

A continuación, se realiza el procedimiento para obtener las palabras más comunes en los tweets de Elon (excluyendo palabras que no aportan información).

In [25]:
tweetss = em_tweets.loc[:,'tweet']
conc = ""
for sstring in tweetss:
    conc = conc + sstring

Se agregarán un par de frases que serían un buen marcador de texto que se debe ignorar, de todas formas ya están consideradas las palabras que comúnmente no aportan significado a una oración.

In [26]:
my_stop_words = [u' ',u'  ', u'http',u'amp']
for stopword in my_stop_words:
    lexeme = nlp.vocab[stopword]
    lexeme.is_stop = True
In [27]:
doc = nlp(conc)
In [28]:
# we add some words to the stop word list
counter = 0
texts, article = [], []
for w in doc:
    # if it's not a stop word or punctuation mark, add it to our article!
    if w.text != '\n' and not w.is_stop and not w.is_punct and not w.like_num:
        # we add the lematized version of the word
        article.append(w.lemma_)
    # if it's a new line, it means we're onto our next document
    if w.text == '\n':
        counter += 1
article = [article]
In [29]:
bigram = gensim.models.Phrases(article)
texts = [bigram[line] for line in article]
trigram = gensim.models.Phrases(texts)
texts1 = [trigram[line] for line in texts]
texts[0][:10]
Out[29]:
['ignore',
 'prior',
 'tweet',
 'pretend',
 'actually',
 'go',
 'Iceland',
 'Sat',
 'ride',
 'bumper']
In [30]:
dictionary = Dictionary(texts)
corpus = [dictionary.doc2bow(text) for text in texts]
In [31]:
elem_dict, index_dict, index_corpus, frequency_corpus = [], [], [], []

for k, v in dictionary.token2id.items():
    elem_dict.append(k)
    index_dict.append(v)

for i in corpus[0]:
    index_corpus.append(i[0])
    frequency_corpus.append(i[1])

elem_1 = np.array(elem_dict)
index_1 = np.array(index_dict)
index_2 = np.array(index_corpus)
frequency_1 = np.array(frequency_corpus)

dict_df = pd.DataFrame({"indice" : index_1,"elemento" : elem_1})
corpus_df = pd.DataFrame({"indice" : index_2,"frecuencia" : frequency_1})

word_frequency = pd.merge(dict_df,corpus_df)
word_frequency = word_frequency.sort_values(by="frecuencia",ascending=False)

word_frequency.iloc[0:10]
Out[31]:
indice elemento frecuencia
4870 4870 Tesla 610
6463 6463 car 405
936 936 @Tesla 352
8638 8638 good 295
12296 12296 like 225
12926 12926 need 215
12178 12178 launch 210
16807 16807 work 197
3932 3932 Model 182
15855 15855 time 169
In [32]:
freq_dict = {}
word_freq = word_frequency.values
for i in range(word_freq.shape[0]):
    freq_dict[word_freq[i, 1]] = word_freq[i, 2]

wordc = WordCloud(width=1920,
                  height=1080,
                  background_color="white",
                  max_words=100,
                  random_state = 97).generate_from_frequencies(freq_dict)
plt.figure()
plt.imshow(wordc, interpolation='bilinear')
plt.axis("off")
plt.savefig('WordMap.png',dpi=300)

Al observar la imagen a anterior, se nota la alta presencia de palabras que hacen referencia a la empresa Tesla, Inc. ya sea de manera directa o indirecta. Por ejemplo, dos de las palabras más utilizadas son Tesla y @Tesla. Además, palabras como car y model, muestran una predominancia en un segundo plano. Todos estos términos pueden relacionarse con Tesla, Inc.

En conjunto con esto, se observan palabras que hacen referencia a otras empresas de las que Elon Musk forma parte, en específico, el termino, landing, launch y SpaceX hacen mención a la empresa de transporte aeoespacial SpaceX.

In [33]:
word_frequency.iloc[0:10,:].plot.bar(x='elemento', y='frecuencia')
plt.grid()
plt.title("10 palabras (significativas) más comúnes en los tweets de Elon Musk")
plt.xlabel("Palabra")
plt.ylabel("Frecuencia")
Out[33]:
Text(0, 0.5, 'Frecuencia')

A continuación se agrega un dataset que muestra el precio de la acción de Tesla, Inc por día, indicando el precio de entrada, de salida, el máximo, el mínimo, el día y el volumen de dinero transado en un día. Se detalla que este dataset se obtuvo desde el sitio web Yahoo.

In [34]:
price = pd.read_csv("TSLA.csv", delimiter=',', header=0)
print(price.head())
         Date       Open   High        Low      Close  Adj Close    Volume
0  2010-06-29  19.000000  25.00  17.540001  23.889999  23.889999  18766300
1  2010-06-30  25.790001  30.42  23.299999  23.830000  23.830000  17187100
2  2010-07-01  25.000000  25.92  20.270000  21.959999  21.959999  8218800 
3  2010-07-02  23.000000  23.10  18.709999  19.200001  19.200001  5139800 
4  2010-07-06  20.000000  20.00  15.830000  16.110001  16.110001  6866900 

En el gráfico a continuación, se muestra la evolución del precio de la acción de Tesla, Inc, donde además se aplico un filtro suavizador de tipo moving average para tener una noción de la tendencia que siguen los datos, dado que naturalemente las acciones son un mercado con altas fluctuaciones día a día, por lo que al graficar los datos sin preprocesar se tiene una muy alta frecuencia que no permite evidenciar de manera muy clara la tendencia.

In [35]:
converted_dates = list(map(datetime.datetime.strptime, price["Date"], price["Date"].values.shape[0]*['%Y-%m-%d']))
x_axis = converted_dates
formatter = dates.DateFormatter('%Y-%m-%d')

y_list_names = list(price.columns[1:-2])

plt.figure()
for name in y_list_names:
    window = 15
    y = price[name]
    avg_mask = np.ones(window)/window
    y_conv = np.convolve(y,avg_mask,'valid')
    plt.plot(x_axis[:-14], y_conv, '-', label=name)
plt.title("Precio de las acciones de Tesla Inc. (TSLA) en el tiempo")
plt.xlabel("Fecha")
plt.ylabel("Precio por acción ($USD)")
plt.legend();

Dado que se observa que los distintos identificadores del precio de la acción muestran un comportamiento similar, se elige uno en particular para seguir trabajando, en específico, se toma como referencia al valor Open.

In [36]:
y_eje=price["Open"]
window = 15
avg_mask = np.ones(window)/window
y_conv = np.convolve(y_eje,avg_mask,'same')
plt.figure(figsize=(15,8))
plt.plot(x_axis[:-14],y_conv[:-14],linewidth=5)
plt.title("Precio de las acciones de Tesla Inc. en el tiempo (Open)")
plt.xlabel("Año")
plt.ylabel("Precio por acción ($USD)")
plt.savefig('accion.png',dpi=300);

En el gráfico anterior se muestra el precio de apertura (Open) diario de la acción de Tesla, Inc., abarcando el período de junio de 2010 hasta la actualidad. Se observa que en el período previo a 2013, la fluctuación del precio estaba acotada y mostraba un comportamiento casi constante respecto al tiempo.

Pese a esto, para los siguientes años se pueden notar claramente dos periodos de tiempo en que las acciones tienen alzas sostenidas, desde 2013 en adelante el precio muestra un incremento considerable, alcanzando aproximadamente cinco veces su valor en un año, donde se estabiliza por los siguientes tres años.

Luego, a finales de 2017 vuelve a mostrar un incremento en el precio. Justamente estos periodos correspondes a momentos posteriores a lanzamientos de nuevos vehículos, ya sea el Tesla Model S, que fue lanzado a mediados del 2012 y Tesla Model 3 que fue presentado a principios del 2016, con las primeras entregas a mediados del 2017.

Experimentos

Para realizar experimentos con los datos, se decidió por tratar en primera instancia los tweets y el precio de la acción por separado. Dado esto a continuación se explica el tratamiento de los tweets.

Vectorización de oraciones para Clustering

Se entrena un modelo utilizando word2vec, que realiza una distribución vectorial de las palabras de los tweets considerando la distantancia de hamming, es decir, mientras más similares son las palabras estarán más cercanas. Este modelo comienza ubicando la primera palabra recibida en la posición (0,0) y distribiyendo las siguientes según la similitud que tienen.

Primer Modelo: Vectores de 2 dimensiones

En una primera instancia se entrena el modelo para que represente las palabras en 2 dimensiones, pasando luego a una representación en 100 dimensiones. Dado que hay procesos que se deben realizar varias veces, se crean funciones que permitan generalizar los procedimientos de representación y visualización, evitando así repetir de manera innecesaria código.

Por lo anterior, se crea la función model_word2vec la cual a través de un conjunto de tweets entrena un modelo de representación vectorial de palabras. Luego, el modelo se encarga de determinar la representación vectorial de cada tweet y aplicar reducción de dimensionalidad de ser necesario.

Además, se crea la función clusters_plot la cual toma la representación vectorial de un conjunto de tweets y se encarga de aplicar técnicas de clustering sobre los datos, ya sea K-Means o DBSCAN, por último se grafica el resultado del clustering.

Es importante notar que el modelo de representación vectorial asigna a cada palabra un vector en n dimensiones, luego, para poder representar un tweet se decidió por representarlo a través del vector promedio de cada palabra que conforma el tweet.

In [37]:
from gensim.models import Word2Vec
from sklearn.cluster import KMeans, DBSCAN 
from sklearn.manifold import TSNE
In [128]:
def model_word2vec(data, min_count_w = 1, size_rep = 2, window_w = 5, random_seed = 68,
                   TSNE_r = False, ndim_f = None, TSNE_seed = 42):
    new_doc = np.array(data)
    
    # Cleaning data - remove punctuation from every newsgroup text
    sentences = []
    # Go through each text in turn
    for ii in range(len(new_doc)):
        sentences = [re.sub(pattern=r'[\!"#$%&\*+,-./:;<=>?@^_`()|~=]', 
                            repl='', 
                            string=x
                           ).strip().split(' ') for x in new_doc[ii].split('\n') 
                          if not x.endswith('writes:')]
        sentences = [x for x in sentences if x != ['']]
        new_doc[ii] = sentences
    
    all_sentences = []
    for text in new_doc:
        all_sentences += text
    
    model = gensim.models.Word2Vec(all_sentences, min_count = min_count_w,  
                                  size = size_rep, window = window_w, seed = random_seed)
    
    def mean_vector(sentence , model):
        new_sen = sentence.split(" ")
        n = 0
        result = np.zeros(size_rep,dtype=float)
        for word in new_sen:
            if word in model.wv.vocab:
                result += model.wv[word]
                n+=1
        if n==0: return result
        return result/n
    
    tweets = np.array([data.apply(lambda x:mean_vector(x,model))])
    tweets = tweets[0,:,:]
    
    if TSNE_r:
        tweets = TSNE(n_components=ndim_f,random_state=TSNE_seed).fit_transform(tweets)
    
    return tweets
In [149]:
def clusters_plot(data, n_class = 2, method = 'KMeans', rand_st = np.random.randint(1), x_label='Eje X', y_label='Eje Y',
                 title = None, leg_loc = 0, leg_size = 2, ms = 3, print_samples = False, org_data = None, n_samples = 5,
                 eps_c=5.5, min_samples_c=2):
    if method == 'KMeans':
        metodo = KMeans(n_clusters=n_class,random_state=rand_st).fit(data)
        centroids = metodo.cluster_centers_
        
    elif method == 'DBSCAN':
        metodo = DBSCAN(eps=eps_c, min_samples=min_samples_c).fit(data)
        
    labels = metodo.labels_
    if not title:
        title = f'{len(set(labels))} Clusters de Tweets'
    
    plt.figure()
    for i in set(labels):
        plt.scatter(data[labels==i,0],
                 data[labels==i,1],
                 label = f'Cluster {i}',
                 s = ms)
    if method == 'KMeans':
        plt.plot(centroids[:,0],centroids[:,1],'ko',markersize=8, label='centroides')
    plt.legend(loc=leg_loc,markerscale=leg_size);
    plt.xlabel('eje X')
    plt.ylabel('eje Y')
    plt.title(title)
    
    if print_samples:
        if org_data is None: 
            print('Datos originales faltantes')
        else:
            for i in range(len(set(labels))):
                print(f'Muestras de la etiqueta {i}:'), print('')
                print(org_data['tweet'].iloc[labels == i][:n_samples]), print('')
    return metodo
In [40]:
tweets_0001 = model_word2vec(em_tweets['tweet'])

En un primer intento de representación se opto por 2 dimensiones, dado que un modelo de esta forma otorga la posibilidad de visualizar los datos y así explorar separaciones naturales en los tweets. A continuación se presenta el gráfico obtenido con esta representación.

In [41]:
plt.scatter(tweets_0001[:,0], tweets_0001[:,1], s = 3)
plt.xlabel('eje X')
plt.ylabel('eje Y')
plt.title('Representacion de Tweets en el espacio');

Experimento 1.1 : K-Means en representación de 2 dimensiones con 2 clusters

Se observa que la representación en dos dimensiones no logra captar una diferencia notable entre los datos, teniendo una rperesentación que se ajusta a una recta. A pesar de lo anterior, se realiza clustering utilizando K-means, en esta primera instancia se grafica para 2 cluster dejando en evindencia los centroides utilizados. También se entrega una muestra de los elementos que se encuentran en cada uno de los cluster.

In [42]:
clusters_plot(tweets_0001, n_class = 2, rand_st = 10, print_samples = True, org_data = em_tweets);
Muestras de la etiqueta 0:

2818    Please ignore prior tweets, as that was someone pretending to be me :)  This is actually me.                                                
2817    Went to Iceland on Sat to ride bumper cars on ice!  No, not the country, Vlad's rink in Van Nuys. Awesome family fun :) http://t.co/rBQXJ9IT
2816    I made the volume on the Model S http://t.co/wMCnT53M go to 11.  Now I just need to work in a miniature Stonehenge...                       
Name: tweet, dtype: object

Muestras de la etiqueta 1:

2806    Yum! Even better than deep fried butter: http://t.co/Ody21NuD                                                                     
2793    RT @TheOnion: Vending Machine Attendant Admits B3 Selection Has Changed A Lot Over The Years http://t.co/nccSGzCQ #OnionInnovation
2791    @TheOnion So true :)                                                                                                              
Name: tweet, dtype: object

Se observa los clusters no contienen información relevante que apriori se pueda utilizar en el proyecto, esto dado que la representación en dos dimensiones pierde la capacidad de mostrar agrupaciones naturales de los datos.

Experimento 1.2 : K-Means en representación de 2 dimensiones con 3 clusters

Antes de pasar a modelos de mayor dimensión, se realiza el mismo procedimiento, pero buscando 3 cluster. Nuevamente se grafica y se representa la ubicación de los centroides, además de entregar una muestra de los elementos pertenecientes a cada cluster

In [43]:
clusters_plot(tweets_0001, n_class = 3, rand_st = 10, print_samples = True, org_data = em_tweets);
Muestras de la etiqueta 0:

2817    Went to Iceland on Sat to ride bumper cars on ice!  No, not the country, Vlad's rink in Van Nuys. Awesome family fun :) http://t.co/rBQXJ9IT
2815    Great Voltaire quote, arguably better than Twain. Hearing news of his own death, Voltaire replied the reports were true, only premature.    
2814    That was a total non sequitur btw                                                                                                           
Name: tweet, dtype: object

Muestras de la etiqueta 1:

2818    Please ignore prior tweets, as that was someone pretending to be me :)  This is actually me.                         
2816    I made the volume on the Model S http://t.co/wMCnT53M go to 11.  Now I just need to work in a miniature Stonehenge...
2812    Read "Lying", the new book by my friend Sam Harris.  Excellent cover art and lots of good reasons not to lie!        
Name: tweet, dtype: object

Muestras de la etiqueta 2:

2793    RT @TheOnion: Vending Machine Attendant Admits B3 Selection Has Changed A Lot Over The Years http://t.co/nccSGzCQ #OnionInnovation
2790    RT @TheOnion: Any Idiot Could Have Come Up With The Car http://t.co/e9cLgfEg #OnionInnovation                                     
2779    China unveils ambitious space strategy http://t.co/udHmuft2                                                                       
Name: tweet, dtype: object

Nuevamente se observa que los clusters no contienen información representativa que permita establecer una diferencia entre ellos, esto implica que se deben utilizar modelos de representación en espacios de más dimensiones, dado que es posible que esto permita obtener más características de los datos.

Segundo Modelo: Vectores de 100 dimensiones y reducción de dimensionalidad con TSNE (100 a 2 dim)

Se entrena otro modelo vectorial con las mismas palabras de los Tweets, pero esta vez la dimensionalidad se aumenta a 100. Aumentar la dimensionalidad entrega una mayor diferenciación entre las palabras. Luego se realiza el cálculo para obtener la posicion de los tweets y finalmente se aplica una reducción de dimensionalidad para tener una representación visual de 2 dimensiones de los datos.

In [141]:
tweets_0002 = model_word2vec(em_tweets['tweet'], size_rep = 100, random_seed = 47,
                             TSNE_r = True, ndim_f = 2, TSNE_seed = 42)
In [142]:
plt.plot(tweets_0002[:,0],tweets_0002[:,1],'b.',alpha=.5)
plt.xlabel('eje X')
plt.ylabel('eje Y')
plt.title('Reducción de 100 a 2 dimensiones');

Se observa que esta representacion muestra agrupaciones de datos, sobre los cuales se pueden explorar sus características para luego obtener información útil para el proyecto. Se utilizará tanto K-means como DBSCAN.

Experimento 2.1 : K-Means con 2 clusters

A la representación vectorial reducida de 100 a 2 dimensiones se le aplica K-means para encontrar 2 cluster, los que se representan en el gráfico, en conjunto a sus centroides. Prosteriormente se entrega una muetra de los datos obtenidos en cada cluster.

In [46]:
clusters_plot(tweets_0002, n_class = 2, rand_st = 10, print_samples = True, org_data = em_tweets);
Muestras de la etiqueta 0:

2818    Please ignore prior tweets, as that was someone pretending to be me :)  This is actually me.                                                
2817    Went to Iceland on Sat to ride bumper cars on ice!  No, not the country, Vlad's rink in Van Nuys. Awesome family fun :) http://t.co/rBQXJ9IT
2816    I made the volume on the Model S http://t.co/wMCnT53M go to 11.  Now I just need to work in a miniature Stonehenge...                       
Name: tweet, dtype: object

Muestras de la etiqueta 1:

2815    Great Voltaire quote, arguably better than Twain. Hearing news of his own death, Voltaire replied the reports were true, only premature.
2814    That was a total non sequitur btw                                                                                                       
2810    Why does the crowd cry over the glorious leader Kim Il Sung's death?  Fear of being shot may play a role:  http://t.co/hoQrYtG1         
Name: tweet, dtype: object

Experimento 2.2 : K-Means con 3 clusters

Se repite el procedimiento para la obtención de 3 cluster, entregando los resultados correspondientes.

In [47]:
clusters_plot(tweets_0002, n_class = 3, rand_st = 10, print_samples = True, org_data = em_tweets);
Muestras de la etiqueta 0:

2815    Great Voltaire quote, arguably better than Twain. Hearing news of his own death, Voltaire replied the reports were true, only premature.
2814    That was a total non sequitur btw                                                                                                       
2810    Why does the crowd cry over the glorious leader Kim Il Sung's death?  Fear of being shot may play a role:  http://t.co/hoQrYtG1         
Name: tweet, dtype: object

Muestras de la etiqueta 1:

2818    Please ignore prior tweets, as that was someone pretending to be me :)  This is actually me.                                                
2817    Went to Iceland on Sat to ride bumper cars on ice!  No, not the country, Vlad's rink in Van Nuys. Awesome family fun :) http://t.co/rBQXJ9IT
2816    I made the volume on the Model S http://t.co/wMCnT53M go to 11.  Now I just need to work in a miniature Stonehenge...                       
Name: tweet, dtype: object

Muestras de la etiqueta 2:

2812    Read "Lying", the new book by my friend Sam Harris.  Excellent cover art and lots of good reasons not to lie!                              
2802    Kanye stopped by the SpaceX rocket factory today. http://t.co/6z7gHBn6                                                                     
2800    The Russians are having some challenges with their rockets. Too many of the engineers that designed them have retired: http://t.co/rEs7spSU
Name: tweet, dtype: object

Se observa que K-Means no otorga buenos resultados ni con dos ni con tres clusters, y es probable que este resultado se mantenga al aumentar el número de clusters. Esto se tiene dado que los datos se distribuyen de forma dispersa en el plano, luego, se hace más relevante detectar zonas de densidad de puntos. El algoritmo que ayuda en datos con características como las anteriores es DBSCAN.

Experimento 2.3 : Clustering mediante DBScan

Cambiando el método para obtener los clusters a DBScan, se fija un eps=4, para obtener una cantidad de cluster que sean bien definidos dentro de las 2 dimensiones, luego se grafica cada uno de los cluster obtenidos y se entrega una muestra de los tweet presentes en cada uno de ellos.

In [143]:
dbscan_01 = clusters_plot(tweets_0002, method = 'DBSCAN', eps_c = 4, rand_st = 10, print_samples = True, org_data = em_tweets);
Muestras de la etiqueta 0:

2818    Please ignore prior tweets, as that was someone pretending to be me :)  This is actually me.                         
2816    I made the volume on the Model S http://t.co/wMCnT53M go to 11.  Now I just need to work in a miniature Stonehenge...
2813    Am reading a great biography of Ben Franklin by Isaacson. Highly recommended.                                        
Name: tweet, dtype: object

Muestras de la etiqueta 1:

2817    Went to Iceland on Sat to ride bumper cars on ice!  No, not the country, Vlad's rink in Van Nuys. Awesome family fun :) http://t.co/rBQXJ9IT
2700    Why I'm leaving the Empire, by Darth Vader  http://t.co/D5YUbAyO                                                                            
2542    View from landing leg camera on Falcon rocket test rig (aka Grasshopper Project)  http://t.co/xhFe87AM                                      
Name: tweet, dtype: object

Muestras de la etiqueta 2:

2815    Great Voltaire quote, arguably better than Twain. Hearing news of his own death, Voltaire replied the reports were true, only premature.
2814    That was a total non sequitur btw                                                                                                       
2806    Yum! Even better than deep fried butter: http://t.co/Ody21NuD                                                                           
Name: tweet, dtype: object

Muestras de la etiqueta 3:

2812    Read "Lying", the new book by my friend Sam Harris.  Excellent cover art and lots of good reasons not to lie!                                          
1701    RT @Extinction_OPS: We're losing species faster than we can count. Join #RacingExtinction in the race to stop the next mass extinction.  ht\xe2\x80\xa6
1371    RT @DiscoveryUK: .@elonmusk we'd love it if you could share our trailer for @louiepsihoyos' film #RacingExtinction  https://t.co/ETrMzkFB1M\xe2\x80\xa6
Name: tweet, dtype: object

Muestras de la etiqueta 4:

2810    Why does the crowd cry over the glorious leader Kim Il Sung's death?  Fear of being shot may play a role:  http://t.co/hoQrYtG1
2809    His singing and acting talent will be sorely missed:  http://t.co/IIFKob75\nSouth Park sequel coming soon...                   
2805    Raul Campos invited me to do a guest DJ gig on KCRW.  Hear my random holiday season music selections at http://t.co/o6FQASvC   
Name: tweet, dtype: object

Muestras de la etiqueta 5:

2793    RT @TheOnion: Vending Machine Attendant Admits B3 Selection Has Changed A Lot Over The Years http://t.co/nccSGzCQ #OnionInnovation               
2589    RT @nprnews: 'Heat Dome' Linked To Greenland's Biggest Melt In 30 Years http://t.co/iUgXF56e                                                     
2441    Busted \xe2\x80\x9c@TheOnion: Barack Obama: 'Look, I'm Just Going To Say It: I Collect Antique Nazi Memorabilia' http://t.co/KExXRoBL\xe2\x80\x9d
Name: tweet, dtype: object

Muestras de la etiqueta 6:

2779    China unveils ambitious space strategy http://t.co/udHmuft2                                               
2670    RT @TheOnion: Pekingese Really Letting Self Go Since Winning Westminster #OnionReview http://t.co/Voj0MZrK
2444    Our Great^n Grandfather http://t.co/zxndg3WF                                                              
Name: tweet, dtype: object

Muestras de la etiqueta 7:

2679    Great work by APJ \xe2\x80\x9c@brynmooser: The foundation is nearly complete at the @artistsforpeace school  http://t.co/hoTXytsZ\xe2\x80\x9d
2416    RT @lenfeldman: .@NYTjamescobb @elonmusk  As your own public editor pointed out, @jbrodernyt was far from a "consummate pro," and you fa ... 
1795    Just got word that the cumulative miles of the  worldwide Tesla fleet passed half a billion!                                                 
Name: tweet, dtype: object

Muestras de la etiqueta 8:

2666    Flight computer aborted rocket hold down firing. Anomaly addressed. Cycling systems to countdown http://t.co/iUi4XdQ3
2629    Space Station tracking spaceship docking approach vector  #Dragon http://t.co/CetZfiNg                               
2499    Liam Neeson\'s "Life\'s Too Short" sketch is super funny http://t.co/nQL7wOx3                                        
Name: tweet, dtype: object

Muestras de la etiqueta 9:

2646    T minus 10 minutes ... Entering terminal count #dragonlaunch      
2645    T minus 60 seconds. Terminal autosequence initiated. #DragonLaunch
2503    @Thomas_Tregner Exactly!                                          
Name: tweet, dtype: object

Muestras de la etiqueta 10:

2420    RT @TeslaRoadTrip: #TeslaRoadTrip All - thanks so much for following our twitter feed.  The trip was a success and everyone has diverted ...                                                
2980    If heat death will be inevitable end of Universe, it actually *is* all about journey                                                                                                        
3408    @Tesla @PopMech Still get this question a lot, even though Norway  has highest Tesla cars per person on  (super appreciated!). Also, AWD is nice to have, but def not required for snow/ice.
Name: tweet, dtype: object

Muestras de la etiqueta 11:

2320    @DreWeathers Exactly              
2157    @1stHubbleFan @keithwarren Exactly
937     Exactly https://t.co/cy1AbIIo42   
Name: tweet, dtype: object

Muestras de la etiqueta 12:

1937    @cairnz 80% to 30%                                
1878    @westcoastbill First, the cheese. Then, the mouse.
463     @La3_id @TheOnion  https://t.co/qOyH1VVBVp        
Name: tweet, dtype: object

Muestras de la etiqueta 13:

1553    Fossil fuels subsidised by $10m a minute, says IMF  http://t.co/c4nsZjXc32                                                                             
1213    RT @marcthiessen: I interviewed .@elonmusk at @AEI World Forum and @realDonaldTrump never came up.  Discussed his plans for manned Mars mis\xe2\x80\xa6
995     RT @Nashenas88: @TeslaMotors @elonmusk  Fantastic counter to misleading LA times article https://t.co/T4OtNNr8Q0 via @FredericLambert                  
Name: tweet, dtype: object

Muestras de la etiqueta 14:

405    @vicentes Yes    
393    @mwangltg Yes    
345    @craighamnett Yes
Name: tweet, dtype: object

Muestras de la etiqueta 15:

36     @nickg_uk No          
33     @nickg_uk @chouky02 No
171    @AdamyanMichael No    
Name: tweet, dtype: object

Aca se puede observar con mayor claridad clusters que tienen sentido semantico, por ejemplo hay clusters que contienen respuestas cortas como "no", "yes", "exactly", además hay clusters donde se agrupan hashtags. Dado esto, una idea a seguir es eliminar los tweets que a priori parecen no aportar con mayor información para el objetivo del proyecto y así poder realizar clustering nuevamente con los datos más limpios. Cabe destacar que para que la limpieza de datos sea más representativa aún, se debe reentrenar el modelo de representación vectorial, pero quitando los tweets que en esta instancia de detectaron como ruido.

In [146]:
labels_01 = dbscan_01.labels_
non_info_tags_01 = [-1,11,12,14,15]
labels_tags_01 = np.zeros(em_tweets.shape[0])
for i in non_info_tags_01:
    labels_tags_01 = labels_tags_01 + (labels_01==i)

cleaned_tweets_01 = pd.DataFrame(data=em_tweets.values[labels_tags_01==0],columns=['date','tweet'])
print(f'Cantidad de tweets a ser eliminados: {labels_tags_01.sum()}')
print(f'Cantidad de tweets restantes: {cleaned_tweets_01.shape[0]}')
Cantidad de tweets a ser eliminados: 198.0
Cantidad de tweets restantes: 6935
In [147]:
tweets_0003 = model_word2vec(cleaned_tweets_01['tweet'], size_rep = 100, random_seed = 47,
                             TSNE_r = True, ndim_f = 2, TSNE_seed = 42)
In [150]:
dbscan_02 = clusters_plot(tweets_0003, eps_c=4, method = 'DBSCAN', rand_st = 10, print_samples = True, org_data = cleaned_tweets_01);
Muestras de la etiqueta 0:

0     Please ignore prior tweets, as that was someone pretending to be me :)  This is actually me.                           
2     I made the volume on the Model S http://t.co/wMCnT53M go to 11.  Now I just need to work in a miniature Stonehenge...  
14    Hi, I'm Art Garfunkel. Have you heard the sound of silence? Because, you know, it makes a sound... http://t.co/7vgya9xL
15    Model S options are out! Performance in red and black for me.  I will deliver my car in June/July. http://t.co/acnyP4nh
19    I                                                                                                                      
Name: tweet, dtype: object

Muestras de la etiqueta 1:

1    Went to Iceland on Sat to ride bumper cars on ice!  No, not the country, Vlad's rink in Van Nuys. Awesome family fun :) http://t.co/rBQXJ9IT
5    Am reading a great biography of Ben Franklin by Isaacson. Highly recommended.                                                               
6    Read "Lying", the new book by my friend Sam Harris.  Excellent cover art and lots of good reasons not to lie!                               
7    Sam Harris also wrote a nice piece on the awesomeness of Hitchens: http://t.co/fPkLiK3v May the good man RIP.                               
8    Why does the crowd cry over the glorious leader Kim Il Sung's death?  Fear of being shot may play a role:  http://t.co/hoQrYtG1             
Name: tweet, dtype: object

Muestras de la etiqueta 2:

3     Great Voltaire quote, arguably better than Twain. Hearing news of his own death, Voltaire replied the reports were true, only premature.
4     That was a total non sequitur btw                                                                                                       
9     His singing and acting talent will be sorely missed:  http://t.co/IIFKob75\nSouth Park sequel coming soon...                            
12    Yum! Even better than deep fried butter: http://t.co/Ody21NuD                                                                           
27    @TheOnion So true :)                                                                                                                    
Name: tweet, dtype: object

Muestras de la etiqueta 3:

25     RT @TheOnion: Vending Machine Attendant Admits B3 Selection Has Changed A Lot Over The Years http://t.co/nccSGzCQ #OnionInnovation               
229    RT @nprnews: 'Heat Dome' Linked To Greenland's Biggest Melt In 30 Years http://t.co/iUgXF56e                                                     
377    Busted \xe2\x80\x9c@TheOnion: Barack Obama: 'Look, I'm Just Going To Say It: I Collect Antique Nazi Memorabilia' http://t.co/KExXRoBL\xe2\x80\x9d
472    A123 battery company renames itself B456 after bankruptcy (really).                                                                              
554    @realbhuwan @teslamotors Cool :)                                                                                                                 
Name: tweet, dtype: object

Muestras de la etiqueta 4:

28     RT @TheOnion: Any Idiot Could Have Come Up With The Car http://t.co/e9cLgfEg #OnionInnovation                                  
70     @TheOnion I heard Lars von Trier optioned the movie rights                                                                     
98     At the Samovar having tea called "monkey picked iron goddess of mercy". Those monkeys sure know their tea. http://t.co/ctEWczlC
152    Flight computer aborted rocket hold down firing. Anomaly addressed. Cycling systems to countdown http://t.co/iUi4XdQ3          
153    T minus five minutes to flame                                                                                                  
Name: tweet, dtype: object

Muestras de la etiqueta 5:

39     China unveils ambitious space strategy http://t.co/udHmuft2                                               
148    RT @TheOnion: Pekingese Really Letting Self Go Since Winning Westminster #OnionReview http://t.co/Voj0MZrK
374    Our Great^n Grandfather http://t.co/zxndg3WF                                                              
463    There's crazy, there's batsh*t crazy, then there's North Korea crazy... http://t.co/etJmNZqVD1            
469    Telemetry says main parachutes deployed ... now visually confirmed                                        
Name: tweet, dtype: object

Muestras de la etiqueta 6:

172    T minus 10 minutes ... Entering terminal count #dragonlaunch      
173    T minus 60 seconds. Terminal autosequence initiated. #DragonLaunch
315    @Thomas_Tregner Exactly!                                          
356    @TristanLaurent V cute! Merry Xmas.                               
421    Solar array deployment successful                                 
Name: tweet, dtype: object

Se observan clusters más separados y además aumento la cantidad de puntos en cada uno, luego, esto implica que los datos están más limpios que antes y por tanto su división en el espacio es más evidente. Aún así, se pueden observar clusters que contienen información no relevante para el proyecto, luego, se repite el proceso de limpieza quitando estos clusters y reentrenando el modelo.

In [151]:
labels_02 = dbscan_02.labels_
non_info_tags_02 = [-1,1,3]
labels_tags_02 = np.zeros(cleaned_tweets_01.shape[0])
for i in non_info_tags_02:
    labels_tags_02 = labels_tags_02 + (labels_02==i)

cleaned_tweets_02 = pd.DataFrame(data=cleaned_tweets_01.values[labels_tags_02==0],columns=['date','tweet'])
print(f'Cantidad de tweets a ser eliminados: {labels_tags_02.sum()}')
print(f'Cantidad de tweets restantes: {cleaned_tweets_02.shape[0]}')
Cantidad de tweets a ser eliminados: 4090.0
Cantidad de tweets restantes: 2845
In [152]:
tweets_0004 = model_word2vec(cleaned_tweets_02['tweet'], size_rep = 100, random_seed = 47,
                             TSNE_r = True, ndim_f = 2, TSNE_seed = 42)
In [160]:
dbscan_03 = clusters_plot(tweets_0004, method = 'DBSCAN', eps_c = 4,rand_st = 10, print_samples = True, org_data = cleaned_tweets_02, n_samples = 7);
Muestras de la etiqueta 0:

0     Please ignore prior tweets, as that was someone pretending to be me :)  This is actually me.                                         
1     I made the volume on the Model S http://t.co/wMCnT53M go to 11.  Now I just need to work in a miniature Stonehenge...                
6     Hi, I'm Art Garfunkel. Have you heard the sound of silence? Because, you know, it makes a sound... http://t.co/7vgya9xL              
7     Model S options are out! Performance in red and black for me.  I will deliver my car in June/July. http://t.co/acnyP4nh              
8     I                                                                                                                                    
9     It was Xmas, so we brought presents for the kids at the orphanage. They don't usually get much. http://t.co/r8qfluIG                 
12    @richardbranson Liked "Screw Business as Usual" a lot. This approach should be taken to heart by all, as it really is the smart move.
Name: tweet, dtype: object

Muestras de la etiqueta 1:

2     Great Voltaire quote, arguably better than Twain. Hearing news of his own death, Voltaire replied the reports were true, only premature.  
3     That was a total non sequitur btw                                                                                                         
4     His singing and acting talent will be sorely missed:  http://t.co/IIFKob75\nSouth Park sequel coming soon...                              
5     Yum! Even better than deep fried butter: http://t.co/Ody21NuD                                                                             
10    @TheOnion So true :)                                                                                                                      
11    RT @TheOnion: Any Idiot Could Have Come Up With The Car http://t.co/e9cLgfEg #OnionInnovation                                             
13    Interesting Economist article about how humanity's collective actions have created a fundamentally new geological age -- the Anthropocene.
Name: tweet, dtype: object

Muestras de la etiqueta 2:

17     China unveils ambitious space strategy http://t.co/udHmuft2                                               
45     RT @TheOnion: Pekingese Really Letting Self Go Since Winning Westminster #OnionReview http://t.co/Voj0MZrK
54     T minus 10 minutes ... Entering terminal count #dragonlaunch                                              
55     T minus 60 seconds. Terminal autosequence initiated. #DragonLaunch                                        
117    @Thomas_Tregner Exactly!                                                                                  
132    @TristanLaurent V cute! Merry Xmas.                                                                       
142    Our Great^n Grandfather http://t.co/zxndg3WF                                                              
Name: tweet, dtype: object

Muestras de la etiqueta 3:

207    @kirkburgess yes     
356    @nitroc3ll yes       
482    @CatherineMotuz yes  
512    @andrewshiamone yes  
569    @MysteryGuitarM yes  
573    @BasseyE @tegmark yes
602    @preastro yes        
Name: tweet, dtype: object

Muestras de la etiqueta 4:

270     @mcrsqr True                                                                                                                     
1158    @davidshepardson True :)                                                                                                         
1394    @mrcsscco True                                                                                                                   
1838    @JohnnaSabri @RehearsalLot @guiscaesar @surfersnout @aedmondsauthor @barbiesway @TwitterMoments @RepMaxineWaters @PolitiFact True
1901    @steveshmidt @thesheetztweetz @CNBC @Lebeaucarnews True                                                                          
2085    @TechGrlTweeter True                                                                                                             
2125    @Scobleizer @Tesla True                                                                                                          
Name: tweet, dtype: object

Muestras de la etiqueta 5:

342     @BKuppersmith Coming soon                                  
1170    @TrickyDickyChap Coming soon                               
1303    @SmileSimplify @VatsalSngh @businessinsider Coming soon ...
1402    @neilsiegel @Tesla Coming very soon                        
1420    @JosephHuberman @Tesla Coming soon                         
1724    @lexiheft @Tesla Coming soon                               
1730    @teslamarcus1 Coming soon                                  
Name: tweet, dtype: object

Muestras de la etiqueta 6:

692     Ok https://t.co/jJJR28UGrK
1037    @AndreElijah Ok           
1396    @Steven_McKie Ok          
1400    @dd_hogan Ok              
1415    @jlgolson Ok              
1498    @xeokeri Ok               
1509    @themadstone Ok           
Name: tweet, dtype: object

Muestras de la etiqueta 7:

1039    @kyleugh Good point         
1172    @anandmahindra Good point :)
1258    @fcingolani Good point      
1490    @AnubhavArora06 Good point  
1624    @OldManLink Good point      
1940    @KirklandJones Good point   
1971    @lrroberts87 Good point     
Name: tweet, dtype: object

Muestras de la etiqueta 8:

1984    @arstechnica Haha          
2234    @fb1975 @DigitalDunzey Haha
2251    @ChipdTooth Haha           
2254    @trobbu @Tesla Haha        
2337    @nichegamer Haha           
2398    @chrinder Haha             
2447    @IanReyTamayo Haha         
Name: tweet, dtype: object

Muestras de la etiqueta 9:

Series([], Name: tweet, dtype: object)

En el último gráfico se observan clusters con mayor concentración de datos y además se observan menos clusters de datos con información no relevante, luego se realiza por última vez el proceso de limpiar los datos quitando los tweets que no aportan para el proyecto.

In [161]:
print(cleaned_tweets_02.shape[0])
print(dbscan_03.labels_.shape)
tweets_0004.shape
2845
(2845,)
Out[161]:
(2845, 2)
In [162]:
labels_03 = dbscan_03.labels_
non_info_tags_03 = [-1,0]
labels_tags_03 = np.zeros(cleaned_tweets_02.shape[0])
for i in non_info_tags_03:
    labels_tags_03 = labels_tags_03 + (labels_03==i)

cleaned_tweets_03 = pd.DataFrame(data=cleaned_tweets_02.values[labels_tags_03==0],columns=['date','tweet'])
print(f'Cantidad de tweets a ser eliminados: {labels_tags_03.sum()}')
print(f'Cantidad de tweets restantes: {cleaned_tweets_03.shape[0]}')
Cantidad de tweets a ser eliminados: 1069.0
Cantidad de tweets restantes: 1776
In [129]:
tweets_0005 = model_word2vec(cleaned_tweets_03['tweet'], size_rep = 100, random_seed = 47,
                             TSNE_r = True, ndim_f = 2, TSNE_seed = 42)
In [130]:
dbscan_04 = clusters_plot(tweets_0005, method = 'DBSCAN', rand_st = 10, print_samples = True, org_data = cleaned_tweets_03);
Muestras de la etiqueta 0:

0    Great Voltaire quote, arguably better than Twain. Hearing news of his own death, Voltaire replied the reports were true, only premature.
1    Yum! Even better than deep fried butter: http://t.co/Ody21NuD                                                                           
2    RT @TheOnion: Vending Machine Attendant Admits B3 Selection Has Changed A Lot Over The Years http://t.co/nccSGzCQ #OnionInnovation      
Name: tweet, dtype: object

Muestras de la etiqueta 1:

86     @kirkburgess yes   
173    @nitroc3ll yes     
248    @CatherineMotuz yes
Name: tweet, dtype: object

Así, es que el último modelo reduce en gran medida la cantidad de clusters, pero al mismo tiempo se observa que disminuyó la cantidad de puntos, esto dado la sucesiva limpieza. Luego, se puede concluir que incluso borrando los datos que son ruido y no aportan a demostrar o rechazar la hipótesis, no se logra encontrar separación evidente entre los clusters. Esto indica que en realidad los tweets de Elon Musk no se clasifican en buenos o malos, o alguna categorización binaria, es más, se observa que muy pocos tweets hablan de su empresa en forma tal que se pueda ver afectado el precio de la acción, luego, los experimentos de clustering permiten concluir que a priori no existe uan relación entre los tweets y el precio de la acción dado que no existen agrupaciones naturales en los tweets. Se buscará confirmar esta afirmación con experimentos de clasificación y regresión.

Tercer Modelo: representación en vectores de 100 dimensiones

Para los tweet del modelo vectorial de 100 dimensiones K-means para distintos valores de k con la finalidad de obtener el optimo mediante el metodo del codo.

Método del codo para cantidad óptima de clusters

In [58]:
from scipy.spatial.distance import cdist

tweets_100 = model_word2vec(em_tweets['tweet'], size_rep = 100, random_seed = 47)
In [59]:
distortions = []
K = range(1,10)
for k in K:
    kmeanModel = KMeans(n_clusters=k,random_state=97).fit(tweets_100)
    kmeanModel.fit(tweets_100)
    distortions.append(sum(np.min(cdist(tweets_100, kmeanModel.cluster_centers_, 'euclidean'), axis=1)) / tweets_100.shape[0])

# Se grafica el codo
plt.plot(K, distortions, 'bx-', linewidth= 5)
plt.xlabel('k cantidad de clusters')
plt.ylabel('Distorción')
plt.title('Método del codo');

Observando el gráfico se puede considerar que el número óptimo de clusters es 3.

Experimento 3.1 : K-Means de 3 clusters con representación de 100 dimensiones

Antes de descartar por completo clustering, se prueba una última vez con una representación en 100 dimensiones sin reducción de dimensionalidad. así, utilizando k=3 se realiza K-means, para obtener los cluster representativos de los tweets, de cada uno de los 3 cluster se encuentra una muetra de algunos de sus tweets.

In [60]:
kmeanModel_opt = KMeans(n_clusters=3,random_state=97).fit(tweets_100)
In [61]:
etiquetas_100 = kmeanModel_opt.labels_
for i in range(len(set(etiquetas_100))):
    print(f'Muestras de la etiqueta {i+1}:'), print('')
    print(em_tweets['tweet'].iloc[etiquetas_100 == i][-5:]), print('')
Muestras de la etiqueta 1:

3635    @techreview Already fixed                               
3636    @kkarthi002 @Tesla Definitely                           
3638    Starhopper https://t.co/VRnAUPjWPt                      
3649    @NCBirbhan Time flies                                   
3647    @annerajb @TeslaMilton @IanPavelko Any user input counts
Name: tweet, dtype: object

Muestras de la etiqueta 2:

3642    RT @cleantechnica: Tesla Model 3 = 60% Of US Electric Vehicle Market https://t.co/4xDU63YNyP https://t.co/UJcPELZerE                                                                                                                                                                                              
3643    @jfinkelstein It will enable video when parked &amp; connected to WiFi. All Tesla Superchargers will have free WiFi over time.                                                                                                                                                                                    
3644    @TeslaMilton @IanPavelko Car looks for a slightly turning force on wheel (direction is irrelevant) to confirm that driver hands are on steering wheel before initiating lane change. Just a precautionary measure until we have a few billion miles/km of real-world lane changes with reliability &gt;&gt; human.
3645    RT @Teslarati: Tesla Model 3 Performance with Peak Power Increase shocks drag racing fans by outrunning a Ferrari 458 https://t.co/pmaa443afy                                                                                                                                                                     
3646    RT @Teslarati: Tesla owners are finding Navigate on Autopilot with no-confirmation lane changes amazingly good https://t.co/RLc76sBr04                                                                                                                                                                            
Name: tweet, dtype: object

Muestras de la etiqueta 3:

3617    "...drivers in this dataset use Autopilot for 34.8% of their driven miles, and yet appear to maintain a relatively high degree of functional vigilance." https://t.co/EZoCCO2bKg
3631    Tesla Enhanced Summon coming out in US next week for anyone with Enhanced Autopilot or Full Self-Driving option                                                                 
3640    @mercilessgains @Tesla Yeah, I'd love to do that                                                                                                                                
3641    @austinbarnard45 You're welcome. Thanks for your great photos!                                                                                                                  
3648    RT @MmeBear7: @elonmusk "We are limited, not by our abilities, but by our vision."    https://t.co/S6bd8hb2OP                                                                   
Name: tweet, dtype: object

Nuevamente se observa que no es posible obtener características relevantes para el proyecto, por lo que se procede con los otros métodos, como clasificación y regresión.

Obtención de Características para la Clasificación.

En esta sección se obtendrán características a partir de los datos, para así aplicar técnicas de Aprendizaje Supervisado como Clasificación y Clustering. Vamos a partir obteniendo el subconjunto de tweets relacionados a Tesla, del dataset original.

In [62]:
tesla_tweets = em_tweets[em_tweets["tweet"].apply(lambda x: "Tesla" in x or "Model" in x or "car" in x)]
In [63]:
tesla_tweets.head()
Out[63]:
date tweet
2817 2011-12-01 09:55 Went to Iceland on Sat to ride bumper cars on ice! No, not the country, Vlad's rink in Van Nuys. Awesome family fun :) http://t.co/rBQXJ9IT
2816 2011-12-01 10:29 I made the volume on the Model S http://t.co/wMCnT53M go to 11. Now I just need to work in a miniature Stonehenge...
2803 2011-12-22 11:30 Model S options are out! Performance in red and black for me. I will deliver my car in June/July. http://t.co/acnyP4nh
2776 2011-12-31 13:06 Hacked my Tesla charge connector on a small island in the rain last night #whatcouldpossiblygowrong
2769 2012-01-01 20:41 If we are not careful, we will find that knife against liberty's neck. Fate has a great sense of irony. http://t.co/OQ2u1zHQ

Filtramos por tweets después del 2012, dado que desde esa fecha hay un flujo constante de tweets.

In [64]:
tesla_tweets = tesla_tweets[tesla_tweets["date"] > "2012-01-01 00:00"]
In [65]:
tesla_stock = price[price["Date"] > "2012-01-01"]

Dado el ruido que existe en el precio de la ación, se estudiará que tamaño de ventana puede ser útil para representar la evolución del precio de la acción, dado que se quiere reducir ruido de la señal, pero no perder la inmediatez que podría tener el efecto de un tweet.

In [66]:
filters = [1, 3, 5, 10, 20]
smoothened_stock = []
for i in filters:
    smoothened_stock.append(np.convolve(tesla_stock["Open"], np.ones((i,))/i, mode='valid'))
In [67]:
for i in range(1, len(smoothened_stock)):
    converted_dates = list(map(datetime.datetime.strptime, tesla_stock["Date"], tesla_stock["Date"].values.shape[0]*['%Y-%m-%d']))
    x_axis = converted_dates
    formatter = dates.DateFormatter('%Y-%m-%d')

    plt.figure(figsize=(15, 10))
    y_axis = tesla_stock["Close"]
    plt.plot(x_axis, y_axis, '-', c='b', label="Señal original")
    aux = str(filters[i])
    plt.plot(x_axis[filters[i] - 1:], smoothened_stock[i], c='r', label="MA sobre la señal, ventana="+aux)
    plt.title("Precio de las acciones de Tesla Inc. (TSLA) en el tiempo", fontsize=24)
    plt.xlabel("Fecha", fontsize=16)
    plt.ylabel("Precio por acción ($USD)", fontsize=16)
    ax = plt.gcf().axes[0] 
    ax.xaxis.set_major_formatter(formatter)
    plt.gcf().autofmt_xdate(rotation=25)
    plt.legend(prop={"size": 16})
    plt.show()

Ahora se obtiene el número de Tweets publicados en ventanas de tiempo anteriores a un dato del dataframe de la acción.

Dados los gráficos obtenidos, se usará la media móvil con tamaño de ventana = 5, dado que se reduce el ruido, pero siguen existiendo saltos no suaves, que podrían codificar la inmediatez del cambio que pudo haber sido inducida por algún tweet.

In [68]:
def simple_features(stock, tweets, days_window, count=False):
    dates = stock["Date"].values.astype("str")
    tweet_dates = tweets["date"]
    num_stock = dates.shape[0]
    features = np.zeros((num_stock - days_window))
    for i in range(days_window, num_stock):
        date = dates[i] + " 00:00"
        prev_date = dates[i - 1]
        n_prev_date = datetime.datetime.strptime(prev_date, "%Y-%m-%d") - datetime.timedelta(days_window - 1)
        aux_month = str(n_prev_date.month).zfill(2)
        aux_day = str(n_prev_date.day).zfill(2)
        n_prev_date_str = f"{n_prev_date.year}-{aux_month}-{aux_day} 00:00"
        num_tweets = tweet_dates[tweet_dates.apply(lambda x: x > n_prev_date_str and x < date)]
        if count:
            features[i - days_window] = int(num_tweets.values.shape[0])
        else:
            features[i - days_window] = int(num_tweets.values.shape[0] > 0)
    return features
In [69]:
features_1 = simple_features(tesla_stock, tesla_tweets, 1)
features_3 = simple_features(tesla_stock, tesla_tweets, 3)
features_1_2 = simple_features(tesla_stock, tesla_tweets, 1, True)
features_3_2 = simple_features(tesla_stock, tesla_tweets, 3, True)

A partir de la media móvil de ventana de tamaño 5, se crea la característica que codifica si la media móvil aumentó de un período a otro.

In [70]:
growth = np.reshape((np.diff(smoothened_stock[2]) > 0).astype(int), (-1, 1))

Se cargan los tweets de Tesla que tienen además el label del sentimiento (positivo, negativo o neutro), asociado a cada tweet, esta se obtuvo etiquetando los tweets a mano.

In [71]:
sentiments = pd.read_csv("sentiment_labels_clean.csv", delimiter=",", index_col=0)

A partir del dataset anterior, se define la característica de sentiment score, que vendría a ser la suma del número de tweets con sentimiento positivo menos el número de tweets con sentimiento negativo, en una ventana de tiempo anterior a un dato de la acción.

In [72]:
def sentiment_score(stock, tweets_labelled, days_window):
    dates = stock["Date"].values.astype("str")
    tweet_dates = tweets_labelled["date"]
    tweet_score = tweets_labelled["label"]
    num_stock = dates.shape[0]
    features = np.zeros((num_stock - days_window))
    for i in range(days_window, num_stock):
        date = dates[i] + " 00:00"
        prev_date = dates[i - 1]
        n_prev_date = datetime.datetime.strptime(prev_date, "%Y-%m-%d") - datetime.timedelta(days_window - 1)
        aux_month = str(n_prev_date.month).zfill(2)
        aux_day = str(n_prev_date.day).zfill(2)
        n_prev_date_str = f"{n_prev_date.year}-{aux_month}-{aux_day} 00:00"
        num_tweets = tweet_score[tweet_dates.apply(lambda x: x > n_prev_date_str and x < date)]
        features[i - days_window] = int(np.sum(num_tweets.values))
    return features

Finalmente, se crean 5 combinaciones de las características anteriormente mencionadas.

  • La primera tiene sólo la característica del número de tweets publicados en el día anterior.
  • La segunda tiene la característica que codifica si la media móvil de la acción aumentó hoy respecto a ayer.
  • La tercera contiene sólo el sentiment score de los tweets del día anterior.
  • La cuarta contiene el número de tweets y el sentiment score.
  • La última tiene las 3 primeras características.

Así la etiqueta será si el precio de la media móvil aumentará mañana respecto a hoy.

In [73]:
x_1 = np.reshape(features_1_2[4:-1], (-1, 1))
x_2 = np.reshape(growth[:-1], (-1, 1))
x_3 = np.reshape(sentiment_score(tesla_stock, sentiments, 1)[4:-1], (-1, 1))
x_4 = np.concatenate((x_1, x_3), axis=1)
x_5 = np.concatenate((x_1, x_2, x_3), axis=1)

y = growth[1:]

Clasificación

In [74]:
from sklearn.model_selection import train_test_split as tts 
In [75]:
from sklearn.tree import DecisionTreeClassifier
from sklearn.naive_bayes import BernoulliNB
from sklearn.metrics import classification_report
from sklearn.metrics import accuracy_score

Al predecir acerca de fluctuaciones en las acciones es común, no hacer un shuffle sobre los datos, es decir se entrena el modelo con datos de una etapa anterior para así ver si el aprendizaje obtenido aplica al futuro.

In [76]:
data = [x_1, x_2, x_3, x_4, x_5]

for i in range(len(data)):
    data.append(tts(data.pop(0), y, test_size=0.3, shuffle=False))

Se usarán dos clasificadores, Árboles de Decisión y Naive Bayes con kernel Bernoulli, para así comparar el rendimiento de los modelos usando los distintos conjuntos de features definidos.

In [77]:
mod_dt = DecisionTreeClassifier()
mod_nb = BernoulliNB()

def fitPredRep(x_train, y_train, x_test, y_test, clf):
    clf.fit(x_train, y_train)
    y_pred = clf.predict(x_test)
    print("Accuracy:", accuracy_score(y_pred, y_test))
    print(classification_report(y_pred, y_test))
    return y_pred

models = ["Num. de tweets ayer", "Crecimiento de media móvil hoy vs ayer", "Sentiment score de tweets de ayer",
         "Num. de tweets ayer y Sentiment Score", "Todas las anteriores"]
dt_pred = []
nb_pred = []

print("Decision Tree Classifier")
for i in range(len(models)):
    print("Features usados:", models[i])
    x_tr, x_te, y_tr, y_te = data[i]
    dt_pred.append(fitPredRep(x_tr, y_tr, x_te, y_te, mod_dt))
    

print("\n\n\n")
print("Bernoulli Naive Bayes")
for i in range(len(models)):
    print("Features usados:", models[i])
    x_tr, x_te, y_tr, y_te = data[i]
    nb_pred.append(fitPredRep(x_tr, y_tr, x_te, y_te, mod_dt))
Decision Tree Classifier
Features usados: Num. de tweets ayer
Accuracy: 0.5256410256410257
              precision    recall  f1-score   support

           0       0.05      0.52      0.08        23
           1       0.96      0.53      0.68       523

   micro avg       0.53      0.53      0.53       546
   macro avg       0.50      0.52      0.38       546
weighted avg       0.92      0.53      0.65       546

Features usados: Crecimiento de media móvil hoy vs ayer
Accuracy: 0.7875457875457875
              precision    recall  f1-score   support

           0       0.78      0.78      0.78       260
           1       0.80      0.80      0.80       286

   micro avg       0.79      0.79      0.79       546
   macro avg       0.79      0.79      0.79       546
weighted avg       0.79      0.79      0.79       546

Features usados: Sentiment score de tweets de ayer
Accuracy: 0.5073260073260073
              precision    recall  f1-score   support

           0       0.08      0.42      0.14        53
           1       0.89      0.52      0.65       493

   micro avg       0.51      0.51      0.51       546
   macro avg       0.49      0.47      0.40       546
weighted avg       0.81      0.51      0.60       546

Features usados: Num. de tweets ayer y Sentiment Score
Accuracy: 0.5091575091575091
              precision    recall  f1-score   support

           0       0.09      0.43      0.15        54
           1       0.89      0.52      0.66       492

   micro avg       0.51      0.51      0.51       546
   macro avg       0.49      0.47      0.40       546
weighted avg       0.81      0.51      0.61       546

Features usados: Todas las anteriores
Accuracy: 0.7289377289377289
              precision    recall  f1-score   support

           0       0.69      0.73      0.71       246
           1       0.77      0.73      0.75       300

   micro avg       0.73      0.73      0.73       546
   macro avg       0.73      0.73      0.73       546
weighted avg       0.73      0.73      0.73       546





Bernoulli Naive Bayes
Features usados: Num. de tweets ayer
Accuracy: 0.5256410256410257
              precision    recall  f1-score   support

           0       0.05      0.52      0.08        23
           1       0.96      0.53      0.68       523

   micro avg       0.53      0.53      0.53       546
   macro avg       0.50      0.52      0.38       546
weighted avg       0.92      0.53      0.65       546

Features usados: Crecimiento de media móvil hoy vs ayer
Accuracy: 0.7875457875457875
              precision    recall  f1-score   support

           0       0.78      0.78      0.78       260
           1       0.80      0.80      0.80       286

   micro avg       0.79      0.79      0.79       546
   macro avg       0.79      0.79      0.79       546
weighted avg       0.79      0.79      0.79       546

Features usados: Sentiment score de tweets de ayer
Accuracy: 0.5073260073260073
              precision    recall  f1-score   support

           0       0.08      0.42      0.14        53
           1       0.89      0.52      0.65       493

   micro avg       0.51      0.51      0.51       546
   macro avg       0.49      0.47      0.40       546
weighted avg       0.81      0.51      0.60       546

Features usados: Num. de tweets ayer y Sentiment Score
Accuracy: 0.5091575091575091
              precision    recall  f1-score   support

           0       0.09      0.43      0.15        54
           1       0.89      0.52      0.66       492

   micro avg       0.51      0.51      0.51       546
   macro avg       0.49      0.47      0.40       546
weighted avg       0.81      0.51      0.61       546

Features usados: Todas las anteriores
Accuracy: 0.7252747252747253
              precision    recall  f1-score   support

           0       0.70      0.72      0.71       254
           1       0.75      0.73      0.74       292

   micro avg       0.73      0.73      0.73       546
   macro avg       0.72      0.72      0.72       546
weighted avg       0.73      0.73      0.73       546

Se encuentra que el modelo de Árboles de Decisión con el feature del crecimiento de la media móvil hoy vs ayer, es el que mejor puede predecir si la media móvil aumentará mañana vs hoy, aún cuando se agregan más features.

A continuación, se muestra el Árbol de decisión que se usa para decidir el alza de la media móvil del precio de la acción:

In [416]:
Image(filename='graph.png')
Out[416]:
In [78]:
from sklearn.metrics import confusion_matrix

def plot_confusion_matrix(y_true, y_pred, 
                          normalize=False,
                          title=None,
                          cmap=plt.cm.Blues):
    """
    This function prints and plots the confusion matrix.
    Normalization can be applied by setting `normalize=True`.
    """
    if not title:
        if normalize:
            title = 'Normalized confusion matrix'
        else:
            title = 'Confusion matrix, without normalization'

    # Compute confusion matrix
    cm = np.array([[186,  70], [54, 237]])
    # Only use the labels that appear in the data
    classes = ["Class 0", "Class 1"]
    if normalize:
        cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
        print("Normalized confusion matrix")
    else:
        print('Confusion matrix, without normalization')

    print(cm)

    fig, ax = plt.subplots()
    im = ax.imshow(cm, interpolation='nearest', cmap=cmap)
    ax.figure.colorbar(im, ax=ax)
    # We want to show all ticks...
    ax.set(xticks=np.arange(cm.shape[1]),
           yticks=np.arange(cm.shape[0]),
           # ... and label them with the respective list entries
           xticklabels=classes, yticklabels=classes,
           title=title,
           ylabel='True label',
           xlabel='Predicted label')

    # Rotate the tick labels and set their alignment.
    plt.setp(ax.get_xticklabels(), rotation=45, ha="right",
             rotation_mode="anchor")

    # Loop over data dimensions and create text annotations.
    fmt = '.2f' if normalize else 'd'
    thresh = cm.max() / 2.
    for i in range(cm.shape[0]):
        for j in range(cm.shape[1]):
            ax.text(j, i, format(cm[i, j], fmt),
                    ha="center", va="center",
                    color="white" if cm[i, j] > thresh else "black")
    fig.tight_layout()
    # plt.savefig("confusion.png")
    return ax

Finalmente, se grafica la matriz de confusión del mejor modelo obtenido.

In [79]:
plot_confusion_matrix(data[1][-1], dt_pred[1], normalize=True, title='Matriz de confusión normalizada');
Normalized confusion matrix
[[0.7265625  0.2734375 ]
 [0.18556701 0.81443299]]

Regresión

A continuación, se desea entrenar un modelo de regresión sobre el precio de la acción de Tesla para si es posible predecir el comportamiento bursátil de la empresa. Dado que hasta el momento se ha considerado como dato sólo el precio de Tesla, se considera necesario agregar otros inputs que otorguen información sobre el comportamiento de mercado en general, de esta manera se puede diferenciar entre lo que fue un cambio en bloque del mercado completo a diferencia de lo que puede ser un comportamiento aislado en el precio de la acción de Tesla.

Dado lo anterior, se consideran dos datasets extras:

  • El índice Nasdaq, el cual muestra el comportamiento de uno de los mercados bursátiles (el de Nueva York) y electrónico más grandes en el mundo. En este mercado se transa la acción de Tesla entre muchas otras, por lo que se cree que el comportamiento de mercado se ve, en cierta medida, representado por este índice.
  • El precio de la acción de la empresa automovilística Ford. Este precio se añadió dado que es una de las empresas con mayor participación en el mercado automovilístico estadounidense, y por tanto, es probable que el precio de su acción contenga información útil para el modelo de regresión.

A continuación, se cargan los datasets especificados y se muestra algunos datos de ejemplo.

In [102]:
nasdaq = pd.read_csv("^IXIC.csv")
ford = pd.read_csv("F.csv")
nasdaq_open = nasdaq["Open"].values
ford_open = ford["Open"].values
In [390]:
print('Muestra del dataset de Nasdaq')
display(nasdaq.head())

print('Muestra del dataset de precio de la acción de Ford')
display(ford.head())
Muestra del dataset de Nasdaq
Date Open High Low Close Adj Close Volume
0 2012-01-03 2657.389893 2665.899902 2641.979980 2648.719971 2648.719971 1636850000
1 2012-01-04 2639.899902 2653.179932 2627.229980 2648.360107 2648.360107 1670530000
2 2012-01-05 2642.570068 2673.560059 2631.229980 2669.860107 2669.860107 1836410000
3 2012-01-06 2671.169922 2682.120117 2658.830078 2674.219971 2674.219971 1683090000
4 2012-01-09 2682.979980 2683.780029 2662.959961 2676.560059 2676.560059 1768080000
Muestra del dataset de precio de la acción de Ford
Date Open High Low Close Adj Close Volume
0 2012-01-03 11.00 11.25 10.99 11.13 8.064753 45709900
1 2012-01-04 11.15 11.53 11.07 11.30 8.187930 79725200
2 2012-01-05 11.33 11.63 11.24 11.59 8.398067 67877500
3 2012-01-06 11.74 11.80 11.52 11.71 8.485017 59840700
4 2012-01-09 11.83 11.95 11.70 11.80 8.550230 53981500
In [131]:
tesla_open = tesla_stock["Open"].values

Se observa que dado que los datasets cuentan con distintas caracterizaciones para el precio en un día, se trabajará con el precio open, esto porque, como se mostró en un inicio, las distintas especificiaciones del precio siguen las mismas tendencias al graficarlas por lo que elegir una por sobre otra no debiera influir en el resultado del experimento.

En el siguiente par de líneas se chequea que los tamaños y las fechas de los precios registrados de los datasets sean iguales.

In [95]:
print(np.array_equal(ford["Date"].values, tesla_stock["Date"].values))
print(np.array_equal(nasdaq["Date"].values, tesla_stock["Date"].values))
True
True

Luego se dividen los datos en train-test.

In [376]:
nasdaq_train, nasdaq_test, ford_train, ford_test = tts(nasdaq_open[:-1], ford_open[:-1], shuffle=False, test_size=0.3)
In [377]:
tesla_train, tesla_test, tesla_open_train, tesla_open_test = tts(tesla_open[:-1], tesla_open[1:], shuffle=False, test_size=0.3)

Después, se normalizan los datos respecto al train set y se crean los features consistentes en los precios de 5 días anteriores, de cada índice económico.

In [378]:
nasdaq_mean, nasdaq_std = np.mean(nasdaq_train), np.std(nasdaq_train)
nasdaq_train_n = (nasdaq_train - nasdaq_mean) / nasdaq_std
nasdaq_test_n = (nasdaq_test - nasdaq_mean) / nasdaq_std
In [379]:
ford_mean, ford_std = np.mean(ford_train), np.std(ford_train)
ford_train_n = (ford_train - ford_mean) / ford_std
ford_test_n = (ford_test - ford_mean) / ford_std
In [380]:
tesla_mean, tesla_std = np.mean(tesla_train), np.std(tesla_train)
tesla_train_n = (tesla_train - tesla_mean) / tesla_std
tesla_test_n = (tesla_test - tesla_mean) / tesla_std
In [381]:
tesla_open_mean, tesla_open_std = np.mean(tesla_open_train), np.std(tesla_open_train)
tesla_open_train_n = (tesla_open_train - tesla_open_mean) / tesla_open_std
tesla_open_test_n = (tesla_open_test - tesla_open_mean) / tesla_open_std
In [382]:
num = nasdaq_train_n.shape[0] - 4

nasdaq_features_tr = np.zeros((num, 5))
ford_features_tr = np.zeros((num, 5))
tesla_features_tr = np.zeros((num, 5))
tesla_open_tr = np.expand_dims(tesla_open_train_n[4:], axis=-1)

for i in range(num):
    nasdaq_features_tr[i, :] = nasdaq_train_n[i:i+5]
    ford_features_tr[i, :] = ford_train_n[i:i+5]
    tesla_features_tr[i, :] = tesla_train_n[i:i+5]
In [383]:
num = nasdaq_test_n.shape[0] - 4

nasdaq_features_te = np.zeros((num, 5))
ford_features_te = np.zeros((num, 5))
tesla_features_te = np.zeros((num, 5))
tesla_open_te = np.expand_dims(tesla_open_test_n[4:], axis=-1)

for i in range(num):
    nasdaq_features_te[i, :] = nasdaq_test_n[i:i+5]
    ford_features_te[i, :] = ford_test_n[i:i+5]
    tesla_features_te[i, :] = tesla_test_n[i:i+5]
In [391]:
train_features_ = np.concatenate((np.ones((tesla_features_tr.shape[0], 1)), 
                                  tesla_features_tr, nasdaq_features_tr, ford_features_tr), axis=-1)
test_features_ = np.concatenate((np.ones((tesla_features_te.shape[0], 1)), 
                                  tesla_features_te, nasdaq_features_te, ford_features_te), axis=-1)

Se define el optimizador de Modelos lineales que usa "Normal Equation" para encontrar el ajuste:

In [385]:
def fit(X_, Y, rho=0):
    # X_: arreglo de características, dim: [m, n_x + 1]
    # Y: arreglo de etiquetas, dim: [m, 1]
    # rho: parametro de regularización
    # Realiza una regresión lineal (lineal no implica orden 1, la regresión es lineal en términos de los parámetros), 
    # del orden especificado para un vector de características y vector de salida (con dimensión 1), 
    # utilizando un parámetro de regularización rho. 

    # Usamos normal equation para obtener los parámetros de la regresión
    Theta = np.linalg.inv(X_.transpose() @ X_ + rho * np.eye(X_.shape[1])) @ X_.transpose() @ Y

    return Theta

Se define la función de costo (Error Cuadrático Medio):

In [386]:
cost = lambda Theta, X_, Y_: np.mean(np.square(np.ravel(X_ @ Theta) - np.ravel(Y_)))

Luego se ajusta el modelo usando varios valores para rho (el parámetro de regularización L2), para encontrar el modelo con mejor rendimineto (menor costo) en el conjunto de test. Se estudiará el ajuste en dos modelos, uno que use todos los índices bursátiles (Tesla, Nasdaq y Ford), que será el modelo A y otro que use sólo los de Tesla, que será el modelo B.

In [409]:
for rho in [0, 2, 5, 10, 15, 20]:
    Theta = fit(train_features_, tesla_open_tr, rho)
    plt.scatter(rho, cost(Theta, train_features_, tesla_open_tr), c='r', edgecolor="k")
    plt.scatter(rho, cost(Theta, test_features_, tesla_open_te), c='b', edgecolor="k")
    Theta = fit(train_features_[:, 0:6], tesla_open_tr, rho)
    plt.scatter(rho, cost(Theta, train_features_[:, 0:6], tesla_open_tr), marker="*", c='r', edgecolor="k", s=120)
    plt.scatter(rho, cost(Theta, test_features_[:, 0:6], tesla_open_te), marker="*", c='b', edgecolor="k", s=120)
plt.ylim([0, 0.03])
plt.title("Error Cuadrático Medio para el Modelo A y B \n usando distintos valores para rho")
plt.ylabel("Error Cuadrático Medio")
plt.xlabel("rho")
plt.legend(["ECM train set, A", "ECM test set, A", "ECM train set, B", "ECM test set, B"])
Out[409]:
<matplotlib.legend.Legend at 0x219976e0ef0>

Se obtiene el mejor rendimiento para rho = 0 (sin regularización), por lo que se usará el Modelo B (más simple) sin regularización.

In [412]:
train_features_ = train_features_[:, :6]
test_features_ = test_features_[:, :6]
Theta = fit(train_features_, tesla_open_tr, rho=0)
Y_pred_train = (train_features_ @ Theta) * tesla_open_std + tesla_open_mean
Y_pred_test = (test_features_ @ Theta) * tesla_open_std + tesla_open_mean
m = test_features_.shape[0]
X_gen = np.ones((m, 6))
X_gen[0, :] = test_features_[0, :]
s = np.sqrt(cost(Theta, train_features_, tesla_open_tr))
for i in range(1, m):
    X_gen[i, 1:5] = X_gen[i-1, 2:]
    X_gen[i, 5] = (X_gen[i-1:i] @ Theta)[0, 0]
Y_pred_gen = (X_gen @ Theta) * tesla_open_std + tesla_open_mean

Graficando el ajuste en el conjunto de train y test:

In [415]:
converted_dates = list(map(datetime.datetime.strptime, tesla_stock["Date"], tesla_stock["Date"].values.shape[0]*['%Y-%m-%d']))
x_axis = converted_dates
formatter = dates.DateFormatter('%Y-%m-%d')

plt.figure(figsize=(15, 10))
y_axis = tesla_stock["Open"]
plt.plot(x_axis, y_axis, '-', c='b', label="Señal original")
plt.plot(x_axis[4:4+train_features_.shape[0]], Y_pred_train, c='r', label="Predición en Conjunto de Train")
plt.plot(x_axis[8+train_features_.shape[0]:8+train_features_.shape[0]+test_features_.shape[0]], Y_pred_test, c='g', 
         label="Predición en Conjunto de Test")
plt.title("Precio de las acciones de Tesla Inc. (TSLA) en el tiempo \n junto con el ajuste de modelos lineales", fontsize=24)
plt.xlabel("Fecha", fontsize=16)
plt.ylabel("Precio por acción ($USD)", fontsize=16)
ax = plt.gcf().axes[0] 
ax.xaxis.set_major_formatter(formatter)
plt.gcf().autofmt_xdate(rotation=25)
plt.legend(prop={"size": 16})
plt.savefig("train_pred.png", dpi=300)

Se observa un buen ajuste en los datos, tanto en el conjunto de train como el de test, dada la calidad del ajuste, se puede observar que bastan sólo los índices de Tesla (no la información obtenida de Tweets u otros índices bursátiles), para predecir el precio de la acción a corto plazo.

Conclusiones

A partir del trabajo realizado tanto de la exploración de datos como de los resultados obtenidos por los experimentos se pueden obtener interpretaciones de los tweets de Elon Musk y las repercuciones que tiene sobre el precio de la acción de la compañia Tesla inc.

De la exploración de datos se puede concluir que Elon Musk en el transcurso de los años ha aumentado la cantidad de Tweets que publica, ya que en el año 2010 solo tweeteo una vez en contraste con los más de 2500 tweet publicados en el año 2018. Dentro de sus tweets también se puede rescatar que las palabras más utilizadas tienen alguna relación con la campañia manejada por Elon Musk, por ejemplo "Tesla", "car", "@Tesla".

Para comenzar a realizar experimentos se realizó una vectorización de los tweets, por lo que se puede dar cuenta de la importancia de la dimensionalidad a utilizar, ya que viendo la representación en 2 dimensiones no se logra apreciar una separación útil de los datos que sirva para realizar clustering, por lo tanto al observar las agrupaciones de datos obtenidos no tienen ninguna representatividad. Por otra parte al aumentar la dimensionalidad y utilizando técnicas de reducción se logran obtener cluster bien definidos. De lo que se puede concluir que trabajar con dimensionalidad alta se transforma en una herramienta muy útil para trabajar datos que son considerados cercanos entre sí en una menor dimensionalidad.

De los cluster mediante DBSCAN obtenidos tampoco se pudo obtener información contundente a partir de los grupos, a excepción de los grupos pequeños que se pueden considerar como ruido.

Para la clasificación se utilizaron distintas cantidades de features y las combinaciones de ellos, tanto para los métodos de árbol de decisión y Bernoulli Naive Bayes. De lo que se puede obtener como conclusión que no se logra establecer una relación directa entre los tweets y la fluctuación del precio de la acción de la compañia, ya que trabajar con los datos de la bolsa entrega mucho mejores resultados de clasificación por sí solos que al agregar los datos de los tweets. Se concluye finalmente que no la hipótesis no tiene validez empírica.

Por el lado de la regresión, se obtuvo que la mejor predicción para el precio futuro de las acciones, se puede obtener con un modelo de Regresión Lineal que usa sólo los datos del índice bursátil de Tesla, donde agregar información de los tweets u otros índices bursátiles no entregaría una mejor predicción. Por lo anterior, se puede concluir que los tweets de Elon Musk no influyen de manera significativa en el precio de la acción, y si alguna vez lo hicieron, fueron sólo situaciones aísladas y no un comportamiento común por parte de Elon (como afirmó la SEC).

In [ ]: